<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<button id="button">User gesture</button>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script>
'use strict';

async function getFrameStatsUntil(track, condition) {
  while (true) {
    const stats = track.stats.toJSON();
    if (condition(stats)) {
      return stats;
    }
    // Repeat in the next task execution cycle.
    await Promise.resolve();
  }
};

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  const firstStats =
    await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
  await getFrameStatsUntil(track,
    stats => stats.totalFrames > firstStats.totalFrames && stats.totalFramesDuration > firstStats.totalFramesDuration);
}, `totalFrames and totalFramesDuration increase over time`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Wait one second for stats
  const stats =
    await getFrameStatsUntil(track, stats => stats.totalFramesDuration > 1000);
  assert_less_than_equal(stats.deliveredFrames, stats.totalFrames);
  assert_less_than_equal(stats.deliveredFramesDuration, stats.totalFramesDuration);
  assert_greater_than_equal(stats.deliveredFrames, 0);
  assert_greater_than_equal(stats.deliveredFramesDuration, 0);
}, `deliveredFrames and deliveredFramesDuration are at most as large as totalFrames and totalFramesDuration`);

promise_test(async t => {
  function assertLatencyStatsInBounds(stats) {
    assert_greater_than_equal(stats.maximumLatency, stats.latency);
    assert_greater_than_equal(stats.maximumLatency, stats.averageLatency);
    assert_less_than_equal(stats.minimumLatency, stats.latency);
    assert_less_than_equal(stats.minimumLatency, stats.averageLatency);
  };

  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());
  const firstStats = track.stats.toJSON();
  assertLatencyStatsInBounds(firstStats);

  // Wait one second for a second stats object.
  const secondStats =
    await getFrameStatsUntil(track, stats => stats.totalFramesDuration - firstStats.totalFramesDuration >= 1000);
  assertLatencyStatsInBounds(secondStats);

  // Reset the latency stats and wait one second for a third stats object.
  track.stats.resetLatency();
  const thirdStats =
    await getFrameStatsUntil(track, stats => stats.totalFramesDuration - secondStats.totalFramesDuration >= 1000);
  assertLatencyStatsInBounds(thirdStats);
}, `Latency and averageLatency is within the bounds of minimumLatency and maximumLatency`);

promise_test(async t => {
  // This behaviour is defined in
  // https://w3c.github.io/mediacapture-extensions/#dom-mediastreamtrackaudiostats-resetlatency
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Wait one second for stats
  const stats =
    await getFrameStatsUntil(track, stats => stats.totalFramesDuration > 1000);
  track.stats.resetLatency();
  assert_equals(track.stats.latency, stats.latency);
  assert_equals(track.stats.averageLatency, stats.latency);
  assert_equals(track.stats.minimumLatency, stats.latency);
  assert_equals(track.stats.maximumLatency, stats.latency);
}, `Immediately after resetLatency(), latency, averageLatency, minimumLatency and maximumLatency are equal to the most recent latency.`);

promise_test(async t => {
  // This behaviour is defined in
  // https://w3c.github.io/mediacapture-extensions/#dfn-expose-audio-frame-counters-steps
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Wait until we have meaningful data
  await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
  const firstStats = track.stats.toJSON();

  // Synchronously wait 500 ms.
  const start = performance.now();
  while(performance.now() - start < 500);

  // The stats should still be the same, despite the time difference.
  const secondStats = track.stats.toJSON();
  assert_equals(JSON.stringify(firstStats), JSON.stringify(secondStats));
}, `Stats do not change within the same task execution cycle.`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Wait for media to flow before disabling the `track`.
  const initialStats = await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
  track.enabled = false;
  // Upon disabling, the counters are not reset.
  const disabledSnapshot = track.stats.toJSON();
  assert_greater_than_equal(disabledSnapshot.totalFramesDuration,
                            initialStats.totalFramesDuration);

  await new Promise(r => t.step_timeout(r, 4000));

  // Frame metrics should be frozen, but because `enabled = false` does not
  // return a promise, we allow some lee-way in case som buffers were still in flight
  // during the disabling.
  assert_approx_equals(
      track.stats.totalFramesDuration, disabledSnapshot.totalFramesDuration, 1000);
}, `Stats are frozen while disabled`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({audio:true});
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  const a = track.stats;
  await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
  const b = track.stats;
  // The counters may have changed, but `a` and `b` are still the same object.
  assert_equals(a, b);
}, `SameObject policy applies`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({audio:true});
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Hold a reference directly to the [SameObject] stats, bypassing the
  // `track.stats` getter in the subsequent getting of `totalFrames`.
  const stats = track.stats;
  const firstTotalFrames = stats.totalFrames;
  while (stats.totalFrames == firstTotalFrames) {
    await Promise.resolve();
  }
  assert_greater_than(stats.totalFrames, firstTotalFrames);
}, `Counters increase even if we don't call the track.stats getter`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({audio:true});
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Wait for 500 ms of audio to flow before disabling the `track`.
  const initialStats = await getFrameStatsUntil(track, stats =>
      stats.totalFramesDuration > 500);
  track.enabled = false;

  // Re-enable the track. The stats counters should be greater than or equal to
  // what they were previously.
  track.enabled = true;
  assert_greater_than_equal(track.stats.totalFrames,
                            initialStats.totalFrames);
  assert_greater_than_equal(track.stats.totalFramesDuration,
                            initialStats.totalFramesDuration);
  // This should be true even in the next task execution cycle.
  await Promise.resolve();
  assert_greater_than_equal(track.stats.totalFrames,
                            initialStats.totalFrames);
  assert_greater_than_equal(track.stats.totalFramesDuration,
                            initialStats.totalFramesDuration);
}, `Disabling and re-enabling does not reset the counters`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({audio:true});
  const [originalTrack] = stream.getTracks();
  t.add_cleanup(() => originalTrack.stop());

  // Wait for 500 ms of audio to flow.
  await getFrameStatsUntil(originalTrack, stats =>
      stats.totalFramesDuration > 500);

  // Clone the track. While its counters should initially be zero, it would be
  // racy to assert that they are exactly zero because media is flowing.
  const clonedTrack = originalTrack.clone();
  t.add_cleanup(() => clonedTrack.stop());

  // Ensure that as media continues to flow, the cloned track will necessarily
  // have less frames than the original track on all accounts since its counters
  // will have started from zero.
  const clonedTrackStats = await getFrameStatsUntil(clonedTrack, stats =>
      stats.totalFramesDuration > 0);
  assert_less_than(clonedTrackStats.totalFrames,
                    originalTrack.stats.totalFrames);
  assert_less_than(clonedTrackStats.totalFramesDuration,
                    originalTrack.stats.totalFramesDuration);
}, `New stats baselines when a track is cloned from an enabled track`);
</script>
